package com.tiny.tinyweb.core.ldap.access;

import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.naming.Name;
import javax.naming.directory.SearchControls;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;

import com.tiny.tinyweb.core.exception.TypeMismatchException;
import com.tiny.tinyweb.core.ldap.entry.MapEntry;
import com.tiny.tinyweb.core.utils.LdapUtils;

/**
 * 基于 {@link org.springframework.ldap.core.LdapTemplate}
 * 的 LDAP 操作类，实现了基本的 CRUD 功能。
 * @see {@link org.springframework.ldap.core.LdapTemplate}
 * @author wxiaobin
 */
public abstract class LdapAccess<T extends MapEntry> {
    /**
     * LDAP 操作模版对象。
     */
    private LdapTemplate ldapTemplate;

    /**
     * 设置 LDAP 操作模版对象。
     * @return odmManager LDAP 操作模版对象的新值
     */
    public void setLdapTemplate(LdapTemplate ldapTemplate) {
        this.ldapTemplate = ldapTemplate;
    }

    /**
     * 将目录查询结果映射到条目对象的 {@link org.springframework.ldap.core.ContextMapper}
     * 对象，该映射器将根据条目属性的名称进行匹配，要求条目对象的字段名必须和目录中属性名称一致。
     * @see {@link org.springframework.ldap.core.ContextMapper}
     */
    private final EntryMapper<T> entryMapper = new EntryMapper<T>();

    /**
     * 设置映射器需要映射的具体的条目类。
     * @param mappedClass 具体的条目类
     */
    public void setManagedClass(Class<T> mappedClass) {
        this.entryMapper.setMappedClass(mappedClass);
    }

    // <editor-fold defaultstate="collapsed" desc="create">
    /**
     * 插入一个新的条目。
     * @param entry 要插入的条目对象
     */
    public void ldapCreate(final T entry) {
        Name dn = this.buildDn(entry);
        DirContextAdapter context = new DirContextAdapter(dn);
        this.mapToContext(entry, context, true);
        this.ldapTemplate.bind(dn, context, null);
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="read">
    /**
     * 根据 DN 查询一个条目。
     * @param entry 保存查询条件的条目对象
     * @return 唯一的条目对象
     */
    @SuppressWarnings("unchecked")
    public T ldapRead(final T entry) {
        return (T) this.ldapTemplate.lookup(
                this.buildDn(entry), this.entryMapper);
    }

    /**
     * 查询指定 DN 下的所有具体条目类对应的条目信息。
     * @param base 要查询的DN，可以为 <code>null</code>，此时基于根 DN 进行查询
     * @param objectClasses 具体条目类所定义的 objectClass 列表
     * @return 指定 DN 下的所有具体条目类对应的条目信息
     */
    @SuppressWarnings("unchecked")
    public List<T> ldapRead(final String base, final List<String> objectClasses) {
        // 根据具体条目类所定义的 objectClass 列表构造过滤器
        AndFilter filter = new AndFilter();
        if (objectClasses == null || objectClasses.isEmpty()) {
            // 默认为指定 DN 下的所有叶子条目
            filter.and(new EqualsFilter("objectClass", "*"));
        } else {
            // 使用 EqualsFilter 构造 AndFilter 过滤器
            for (String objectClass : objectClasses) {
                filter.and(new EqualsFilter("objectClass", objectClass));
            }
        }
        // 执行查询
        return  this.ldapTemplate.search(this.buildDn(base), filter.encode(),
                SearchControls.ONELEVEL_SCOPE, this.entryMapper);
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="update">
    /**
     * 更新指定的条目，参数 <code>entry</code> 中指定的非空属性为更新对象。
     * @param entry 要更新的条目对象
     * @param ignoreNull 是否忽略条目对象中的空值属性，<code>true</code>时忽略
     */
    public void ldapUpdate(final T entry, final boolean ignoreNull) {
        Name dn = this.buildDn(entry);
        DirContextAdapter context = (DirContextAdapter) this.ldapTemplate.lookup(dn);
        this.mapToContext(entry, context, ignoreNull);
        this.ldapTemplate.modifyAttributes(dn, context.getModificationItems());
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="delete">
    /**
     * 删除一个条目，参数 <code>entry</code> 中指定的 DN 为删除条件。
     * @param entry 要删除的条目对象
     */
    public void ldapDelete(final T entry) {
        this.ldapTemplate.unbind(this.buildDn(entry));
    }

    /**
     * 根据 DN 删除条目，包括该条目下的所有子条目。
     * @param base 要删除的条目的 DN
     */
    public void ldapDelete(final String base) {
        this.ldapTemplate.unbind(base, true);
    }
    // </editor-fold>

    /**
     * 根据给定的条目对象，创建该条目的 DN 对象。
     * @param entry 创建给定的条目对象
     * @return 该条目的 DN 对象
     */
    @SuppressWarnings("unchecked")
    private Name buildDn(final T entry) {
        // 初始化 DN 对象
        DistinguishedName dn = new DistinguishedName();
        try {
            // 处理 DN 名称列表中定义的所有构成 DN 的属性，从属性名列的尾部向前处理
            List<String> dnKeys = entry.getDnKeys();
            for (int i = dnKeys.size() - 1; i >= 0; i--) {
                String dnKey = dnKeys.get(i);
                if (entry.get(dnKey) instanceof Collection<?>) {
                    // 多值属性
                    for (String dnValue : (Collection<String>) entry.get(dnKey)) {
                        dn.add(dnKey, dnValue);
                    }
                } else {
                    // 单值属性
                    dn.add(dnKey, (String) entry.get(dnKey));
                }
            }
        } catch(Exception e) {
            // 构成 DN 的属性类型不匹配
            throw new TypeMismatchException(
                    "DN has to built from 'Collection<String>' or 'String'.");
        }
        return dn;
    }

    /**
     * 根据指定的 DN 字符串，创建不含根 DN 的
     * {@link org.springframework.ldap.core.DistinguishedName} 对象。
     * @param fullDn 指定的 DN 字符串，可以不指定 （空值或空字符串），此时返回根 DN 对象
     * @return 不含根 DN 的 DN 对象
     */
    private Name buildDn(final String fullDn) {
        return LdapUtils.buildDn(fullDn,
                ((LdapContextSource) this.ldapTemplate
                        .getContextSource()).getBaseLdapPath());
    }

    /**
     * 将条目对象中的属性映射到目录访问上下文对象中。
     * @param entry 条目对象
     * @param context 目录访问上下文对象
     * @param ignoreNull 是否忽略条目对象中的空值属性，<code>true</code>时忽略
     */
    private void mapToContext(final T entry, DirContextAdapter context,
            boolean ignoreNull) {
        // 获得条目对象的属性描述对象
        Map<String, PropertyDescriptor> properties = entry.getProperties();
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(entry);
        // 映射所有属性
        for (Entry<String, PropertyDescriptor> property : properties.entrySet()) {
            String name = property.getValue().getName();
            Object values = bw.getPropertyValue(name);
            // 如果该属性的值为集合对象，设置复合属性，否则设置单一属性
            if (values instanceof Collection<?>) {
                Collection<?> collection = (Collection<?>) values;
                // 如果忽略空值属性，则删除该空值属性
                if (ignoreNull) {
                    for (Iterator<?> it = collection.iterator(); it.hasNext();) {
                        if (it.next() == null) {
                            it.remove();
                        }
                    }
                }
                // 如果该多值属性不包含任何项目，则跳过该属性
                if (!collection.isEmpty()) {
                    context.setAttributeValues(name, collection.toArray());
                }
            } else {
                // 如果忽略空值属性，则跳过该空值属性
                if (!ignoreNull || values != null) {
                    context.setAttributeValue(name, values);
                }
            }
        }
    }
}
